import { dumpYaml, V1Secret } from "@kubernetes/client-node"
import { ChildProcess, execSync, spawn } from 'child_process'
import * as fs from 'fs'
import { lookpath } from 'lookpath'
import * as os from 'os'
import * as path from 'path'
import { fsAppend, logStream, wait } from "yam-operator/util"
import { K8SResourceType, KV, KVString, OperateFunction, PlanContext } from "yam-types"
import { HelmReleaseConfig } from "../types.d"

const DEFAULT_CHART_REPOS: KVString = { "bitnami": "https://charts.bitnami.com/bitnami"}

const helmOperator: OperateFunction<HelmReleaseConfig> = async (plan, diff, settings) => {
  if (!diff.hasDiff) {
    return
  }
  // TODO: cache the bin path and check result to avoid duplicate operation
  for (const releaseConf of diff.newItems) {
    const chart = releaseConf.chartName || releaseConf.name
    plan.log.info(`helm chart '${chart}', release name: ${releaseConf.name} will be installed`)
    await operateHelmChart(plan, releaseConf, false, false, settings)
  }
  for (const modifiedRelease of diff.modifiedItems) {
    const chart = modifiedRelease.current.chartName || modifiedRelease.current.name
    plan.log.info(`upgrading helm chart '${chart}', release name: ${modifiedRelease.current.name}`)
    await operateHelmChart(plan, modifiedRelease.current, true, false, settings)
    plan.log.info(`helm chart ${modifiedRelease.current.name} upgraded`)
  }
  diff.deletedItems.forEach(r => {
    plan.log.warn(`removing helm release is not allowed, please run 'helm uninstall ${r.name}' command manually`)
  })
}

const helmUninstaller: OperateFunction<HelmReleaseConfig> = async (plan, diff, settings) => {
  for (const releaseConf of diff.currentItems) {
    const chart = releaseConf.chartName || releaseConf.name
    plan.log.info(`helm chart '${chart}', release name: ${releaseConf.name} will be installed`)
    await operateHelmChart(plan, releaseConf, false, true, settings)
  }
}

export const operateHelmChart = async (plan: PlanContext, release: HelmReleaseConfig, isUpdate: boolean, isDelete: boolean, settings?: KV): Promise<void> => {
  const ns = release.namespace || plan.data.namespace
  const helmValConfPath = path.join(os.tmpdir(), ['values', plan.data.workloadName, release.name, Date.now()].join('-') + '.yaml')

  if (isDelete && plan.data.templateOutput) {
    plan.log.info(`skip 'helm template' for Chart(${release.name}) that would be uninstalled`)
    return
  }
  // check and download Helm v3 binary if not exists
  const helmExecutable = await checkHelmExecutableAndUpdateRepo(plan, isDelete || settings?.disableRepoUpdate as boolean, settings)
  let operation = plan.data.templateOutput ? 'template' : isDelete ? 'uninstall' : isUpdate ? 'upgrade' : 'install'
  plan.action(async (ctx) => {
    try {
      if (release.values) {
        fs.writeFileSync(helmValConfPath, dumpYaml(release.values))
      }
      const helmSecrets: V1Secret[] = await ctx.k8sClient.find({
        namespace: ns,
        kind: K8SResourceType.Secret
      }, { "owner": "helm", name: release.name })
      const existing = helmSecrets.find(r => r.type === "helm.sh/release.v1" && r.metadata)
      if (existing) {
        if (isDelete) {
          plan.log.info(`helm release '${release.name}' will be uninstalled.`)
        } else {
          operation = 'upgrade'
          plan.log.info(`helm release '${release.name}' already exists in namespace ${ns}, will convert to 'upgrade' operation.`)
          plan.log.info(`helm release last modified at: ${new Date(parseInt(existing.metadata?.labels?.modifiedAt as string) * 1000).toISOString()}`)
        }
      } else if (isDelete) {
        plan.log.warn(`helm release '${release.name}' is not found, can not uninstall.`)
        return
      }

      // compose command
      const cmd = [operation, release.name, '-n', ns]
      cmd.push('--kube-context')
      cmd.push(plan.data.kubeContextName)
      if (plan.data.kubeConfigPath) {
        cmd.push('--kubeconfig')
        cmd.push(plan.data.kubeConfigPath)
      }
      if (!isDelete) {
        const repo = settings?.repo || release.repo
        if (repo) {
          cmd.push('--repo')
          cmd.push(repo as string)
        }
        if (release.values) {
          cmd.push('-f')
          cmd.push(helmValConfPath)
        }
        // templateOutput not empty means 'yam template' executed
        if (release.wait && !plan.data.templateOutput) {
          cmd.push('--wait')
        }
        if (settings?.username) {
          cmd.push('--username')
          cmd.push(settings.username as string)
        }
        if (settings?.password) {
          cmd.push('--password')
          cmd.push(settings.password as string)
        }
        if (release.version) {
          cmd.push('--version=')
          cmd.push(release.version)
        }
      }
      if (plan.data.dryRun) {
        cmd.push('--dry-run')
      }
      if (!isDelete) {
        cmd.push(release.chartName || release.name)
      }

      // execute command
      ctx.log.info(`---------------- Helm Operator Start ---------------`)
      ctx.log.info(`exec: helm ${settings?.password ? cmd.join(' ').replace(settings.password as string, '***') : cmd.join(' ')}`)
      if (!plan.data.dryRun || plan.data.templateOutput) {
        const proc = spawn(helmExecutable as string, cmd, {
          // redirect output
          stdio: ['inherit', 'pipe', 'pipe']
        })
        let output = ''
        proc.stdout.on('data', buffer => {
          // append to templateOutput file if running in template mode
          if (plan.data.templateOutput) {
            output += buffer.toString()
          }
          logStream.write(buffer)
        })
        proc.stderr.on('data', buffer => {
          logStream.write(buffer)
        })
        await onExit(proc)
        if (plan.data.templateOutput) {
          await fsAppend(plan.data.templateOutput, '\n\n# Generated by Helm-Operator YAM Plugin\n' + output + '\n\n')
        }
        // you might apply too many yaml(s), get some rest for your Kubernetes to avoid 429
        await wait(1000)
      } else {
        ctx.log.info(`exec: dry-run mode. command won't be executed`)
      }
    } finally {
      if (release.values) {
        fs.unlinkSync(helmValConfPath)
      }
      ctx.log.debug(`tmp file ${helmValConfPath} deleted`)
      ctx.log.info(`---------------- Helm Operator End -----------------`)
    }
  }, `helm-${operation}:${release.name}`)
}

function onExit(childProcess: ChildProcess): Promise<void> {
  return new Promise((resolve, reject) => {
    childProcess.once('exit', (code: number) => {
      if (code === 0) {
        resolve(undefined)
      } else {
        reject(new Error('Spawn process exited with error code: ' + code))
      }
    })
    childProcess.once('error', (err: Error) => {
      reject(err)
    })
  })
}

let helmExecutablePath = ''

async function checkHelmExecutableAndUpdateRepo(plan: PlanContext, disableRepoUpdate: boolean, settings?: KV): Promise<string> {
  if (helmExecutablePath) {
    return helmExecutablePath
  }
  plan.log.info('checking helm binary executable')
  const basePath = path.join(os.homedir(), '.yam')
  const helmExecutable = await lookpath('helm', {
    include: [basePath]
  })
  if (!helmExecutable) {
    throw new Error(`helm binary executable is not found, please download and configure your PATH, refer: https://github.com/helm/helm/releases`)
  }
  try {
    const ver = execSync(helmExecutable + ' version').toString()
    if (!ver || ver.indexOf('SemVer:"v2') !== -1) {
      throw new Error(`Helm v2 is not supported, please upgrade to v3 manually, binary path: ${helmExecutable}, ${ver}`)
    }
    plan.log.info(`helm executable: ${ver.substring(ver.indexOf('{') + 1, ver.length - 1)}.`)
    
    if (!settings?.disablePublicRepo) {
      for (const name in DEFAULT_CHART_REPOS) {
        const result = execSync(helmExecutable + ' repo add ' + name + ' ' + DEFAULT_CHART_REPOS[name].toString()).toString()
        plan.log.debug(`${result}public helm repo added.`)
      }
    }  
    if (settings?.repos) {
      const repos = (settings?.repos as KVString)
      for (const name in repos) {
        const result = execSync(helmExecutable + ' repo add ' + name + ' ' + repos[name].toString()).toString()
        plan.log.debug(`${result}customized helm repos added.`)
      }
    }
    if (!disableRepoUpdate) {
      const result = execSync(helmExecutable + ' repo update').toString()
      plan.log.info(`${result}\nhelm repo update finished.`)
    }
  } catch (e) {
    plan.log.error(`can not execute helm command, binary path: ${helmExecutable}. ${e.message}`)
    throw e
  }
  helmExecutablePath = helmExecutable
  return helmExecutablePath
}

export { helmOperator, helmUninstaller }